package jmine.tec.rpc.checkup;

import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import jmine.tec.rpc.RPCMessages;
import jmine.tec.rpc.common.serialization.Serializer;
import jmine.tec.rpc.common.serialization.SerializerBuilder;
import jmine.tec.utils.reflection.GenericTypeResolver;
import jmine.tec.utils.reflection.GenericTypeResolverImpl;

import org.apache.commons.beanutils.ConstructorUtils;

import bancosys.tec.exception.LocalizedMessageHolder;

/**
 * Verifica se os métodos do serviço REST possuem parâmetros que podem ser serializados.
 * 
 * @author lundberg
 */
public class RestServiceSerializationCheckup {

    private GenericTypeResolver resolver = GenericTypeResolverImpl.getInstance();

    private Class<?> service;

    private Serializer serializer;

    private Map<Class<?>, Object> instances = new HashMap<Class<?>, Object>();

    /**
     * Construtor
     * 
     * @param service service
     * @param serializer serializer
     */
    public RestServiceSerializationCheckup(Class<?> service, Serializer serializer) {
        this.service = service;
        this.serializer = serializer;
    }

    /**
     * Adiciona uma instância que será utilizada nos testes de serialização
     * 
     * @param <S> tipo de instância
     * @param sampleClass sampleClass
     * @param sampleInstance sampleInstance
     * @return RestServiceSerializationCheckup
     */
    public <S> RestServiceSerializationCheckup withSample(Class<S> sampleClass, S sampleInstance) {
        this.instances.put(sampleClass, sampleInstance);
        return this;
    }

    /**
     * Construtor
     * 
     * @param service service
     */
    public RestServiceSerializationCheckup(Class<?> service) {
        this(service, SerializerBuilder.aSerializer().build());
    }

    /**
     * Verifica todos os métodos da interface
     * 
     * @throws Exception Exception
     */
    public void checkAllMethods() throws Exception {
        for (Method m : this.service.getMethods()) {
            this.checkMethod(m);
        }
    }

    /**
     * Verifica um único método com a assinatura passada
     * 
     * @param name name
     * @param parameterTypes parameterTypes
     * @throws Exception Exception
     */
    public void checkMethod(String name, Class... parameterTypes) throws Exception {
        this.checkMethod(this.service.getMethod(name, parameterTypes));
    }

    /**
     * Verifica um único método
     * 
     * @param method method
     * @throws Exception Exception
     */
    public void checkMethod(Method method) throws Exception {
        for (Type parameterType : method.getGenericParameterTypes()) {
            this.checkRepresentationSerialization(this.typeAsClass(parameterType));
        }
        for (Type exceptionType : method.getGenericExceptionTypes()) {
            this.checkExceptionSerialization(this.typeAsClass(exceptionType));
        }
        if (!Void.class.equals(this.typeAsClass(method.getGenericReturnType()))) {
            this.checkRepresentationSerialization(this.typeAsClass(method.getGenericReturnType()));
        }
    }

    /**
     * Tenta obter a classe representada pelo tipo
     * 
     * @param type type
     * @return class, retorna null caso não tenha sucesso
     */
    private Class<?> typeAsClass(Type type) {
        if (type instanceof Class) {
            return (Class) type;
        } else if (type instanceof ParameterizedType) {
            return this.typeAsClass(((ParameterizedType) type).getRawType());
        } else if (type instanceof TypeVariable && this.resolver.isResolvable(type, this.service)) {
            return this.resolver.resolveTypeForFixedGenericType((TypeVariable) type, this.service);
        }
        return null;
    }

    /**
     * Verifica a serialização da exceção
     * 
     * @param exceptionType exceptionType
     * @throws Exception Exception
     */
    private void checkExceptionSerialization(Class<?> exceptionType) throws Exception {
        if (this.hasConstructor(exceptionType, String.class)) {
            this.checkException((Throwable) ConstructorUtils.invokeConstructor(exceptionType, "mensagem"));
        } else if (this.hasConstructor(exceptionType, LocalizedMessageHolder.class)) {
            LocalizedMessageHolder holder = RPCMessages.REMOTE_EXCEPTION_MESSAGE.create("mensagem");
            this.checkException((Throwable) ConstructorUtils.invokeConstructor(exceptionType, holder));
        }

    }

    /**
     * Verifica se o tipo possui construtor para os tipos dados
     * 
     * @param type type
     * @param parameterTypes parameterTypes
     * @return boolean
     */
    private boolean hasConstructor(Class<?> type, Class... parameterTypes) {
        try {
            type.getConstructor(parameterTypes);
            return true;
        } catch (NoSuchMethodException e) {
            return false;
        }
    }

    /**
     * Verifica a serialização da exception dada
     * 
     * @param <T> tipo de exception
     * @param throwable throwable
     */
    private <T extends Throwable> void checkException(T throwable) {
        new ThrowableSerializationCheckup<T>(throwable, this.serializer);
    }

    /**
     * Verifica se a representação pode ser serializada. Teste com instância vazia.
     * 
     * @param <R> tipo de representação
     * @param representationClass representationClass
     * @throws Exception Exception
     */
    @SuppressWarnings("unchecked")
    private <R> void checkRepresentationSerialization(Class<R> representationClass) throws Exception {
        if (representationClass == null) {
            return;
        } else if (this.instances.containsKey(representationClass)) {
            new RepresentationSerializationCheckup<R>((R) this.instances.get(representationClass), this.serializer).check();
        } else if (representationClass.isArray()) {
            this.checkRepresentationSerialization(representationClass.getComponentType());
        } else if (representationClass.isEnum()) {
            return;
        } else if (representationClass.isPrimitive()) {
            return;
        } else if (Collection.class.isAssignableFrom(representationClass)) {
            return;
        } else if (Class.class.isAssignableFrom(representationClass)) {
            return;
        } else if (Object.class.equals(representationClass)) {
            return;
        } else {
            new RepresentationSerializationCheckup<R>(representationClass, this.serializer).check();
        }
    }
}
